/*
 * Copyright 2011-2013, David George, Licensed under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package org.magneato.service;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.magneato.dto.MagUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserCache;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.userdetails.cache.NullUserCache;
import org.springframework.security.provisioning.GroupManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.util.Assert;

/**
 * Implementations to register users must adhere to this interface
 * 
 * Users table is slightly extended from the Spring model
 * 
 * 
 * Note: Assumes Apache Derby as the default database. You will need to override
 * selectAllUsers to support paging on your database.
 * 
 * @author David George
 * @author <a href="mailto:cyrille@cyrilleleclerc.com">Cyrille Le Clerc</a>
 */
public class UserManager extends JdbcDaoSupport implements UserDetailsService,
		GroupManager, UserDetailsManager {
	private final Log _logger = LogFactory.getLog(UserManager.class);
	@Value("${reg.salt}")
	private String salt;
	private int userCount;
	@Autowired
	private AuthenticationManager authenticationManager;

	private UserCache userCache = new NullUserCache();
	private static final String createUserSql = "INSERT INTO users (username, password, enabled, screenname, ip_address, join_date) VALUES (?, ?, ?, ?, ?, ?)";
	private static final String createAuthoritySql = "INSERT INTO authorities (id, authority) VALUES (?,?)";
	private final static String updateUserSql = "UPDATE users SET username = ?, password = ?, enabled = ?, screenname = ?, ip_address = ?, join_date = ? where username = ?";
	private final static String updateUserNoPWSql = "UPDATE users SET username = ?, enabled = ?, screenname = ?, ip_address = ?, join_date = ? where username = ?";

	private final static String usersByUsernameSql = "SELECT id, username, password, enabled, screenname, ip_address, join_date from users where username = ?";
	private final static String authoritiesFromIdSql = "select id,authority from authorities where id = ?";
	private final static String deleteUserAuthoritiesSql = "delete from authorities where id = ?";
	private final static String deleteUserSql = "delete from users where username = ?";
	private final static String changePasswordSql = "update users set password = ? where username = ?";
	private final static String userExistsSql = "select username from users where username = ?";
	private String selectUserNameSql = "SELECT username from users where screenname = ?";
	// NOTE: Apache Derby Syntax
	private String selectAllUsersSql = "SELECT id, username, screenname, enabled, ip_address, join_date FROM ( SELECT ROW_NUMBER() OVER() AS rownum, users.* FROM users) AS tmp WHERE rownum > ? AND rownum <= ?";
	private final static String userCountSQL = "SELECT COUNT(*) FROM users";
	private final static String usersByIdSql = "SELECT id, username, password, enabled, screenname, ip_address, join_date from users where id = ?";

	/**
	 * Update {@link ExtendedUser} extra columns in addition to the behavior of
	 * {@link JdbcUserDetailsManager#createUser(UserDetails).}
	 */
	@Override
	public void createUser(UserDetails user) {
		validateUserDetails(user);

		final MagUser extendedUser = (MagUser) user;

		int updatedRows = getJdbcTemplate().update(createUserSql,
				new PreparedStatementSetter() {
					public void setValues(PreparedStatement ps)
							throws SQLException {
						ps.setString(1, extendedUser.getUsername());
						ps.setString(2, extendedUser.getPassword());
						ps.setBoolean(3, extendedUser.isEnabled());
						ps.setString(4, extendedUser.getScreenname());
						ps.setString(5, extendedUser.getIp());
						ps.setDate(6,
								new java.sql.Date(extendedUser.getJoinDate()));
					}
				});

		int id = getJdbcTemplate().queryForObject(
				"select id from users where username = ?",
				new String[] { extendedUser.getUsername() },
				new RowMapper<Integer>() {
					public Integer mapRow(ResultSet rs, int rowNum)
							throws SQLException {
						return rs.getInt(1);
					}
				});

		insertUserAuthorities(id, user.getAuthorities());
		userCount = 0; // force user count to be reloaded
	}

	private void insertUserAuthorities(int id,
			Collection<? extends GrantedAuthority> authorities) {
		for (GrantedAuthority auth : authorities) {
			getJdbcTemplate().update(createAuthoritySql, id,
					auth.getAuthority());
		}
	}

	public void updateUser(final UserDetails user) {
		validateUserDetails(user);
		final MagUser extendedUser = (MagUser) user;
		if (user.getPassword() != null) {
			getJdbcTemplate().update(updateUserSql,
					new PreparedStatementSetter() {
						public void setValues(PreparedStatement ps)
								throws SQLException {
							ps.setString(1, extendedUser.getUsername());
							ps.setString(2, extendedUser.getPassword());
							ps.setBoolean(3, extendedUser.isEnabled());
							ps.setString(4, extendedUser.getScreenname());
							ps.setString(5, extendedUser.getIp());
							ps.setDate(
									6,
									new java.sql.Date(extendedUser
											.getJoinDate()));
							ps.setString(7, extendedUser.getUsername());
						}
					});
		} else {
			getJdbcTemplate().update(updateUserNoPWSql,
					new PreparedStatementSetter() {
						public void setValues(PreparedStatement ps)
								throws SQLException {
							ps.setString(1, extendedUser.getUsername());
							ps.setBoolean(2, extendedUser.isEnabled());
							ps.setString(3, extendedUser.getScreenname());
							ps.setString(4, extendedUser.getIp());
							ps.setDate(
									5,
									new java.sql.Date(extendedUser
											.getJoinDate()));
							ps.setString(6, extendedUser.getUsername());
						}
					});
		}
		deleteUserAuthorities(extendedUser.getId());
		insertUserAuthorities(extendedUser.getId(), user.getAuthorities());

		userCache.removeUserFromCache(user.getUsername());
	}

	private void deleteUserAuthorities(int id) {
		getJdbcTemplate().update(deleteUserAuthoritiesSql, new Object[] { id });
	}

	public UserDetails loadUserByUsername(String username)
			throws UsernameNotFoundException, DataAccessException {
		List<MagUser> users = loadUsersByUsername(username);

		if (users.size() == 0) {
			throw new UsernameNotFoundException("No user in repository "
					+ username);
		}

		MagUser user = users.get(0); // contains no GrantedAuthority[]

		List<GrantedAuthority> dbAuths = loadUserAuthorities(user.getId());
		if (dbAuths.size() == 0) {
			throw new UsernameNotFoundException("no authority for username "
					+ username);
		}

		user.setAuthorities(dbAuths);
		return user;
	}

	/**
	 * Executes the SQL <tt>usersByUsernameQuery</tt> and returns a list of
	 * UserDetails objects. There should normally only be one matching user.
	 */
	protected List<MagUser> loadUsersByUsername(String username) {
		return getJdbcTemplate().query(usersByUsernameSql,
				new String[] { username }, new RowMapper<MagUser>() {
					public MagUser mapRow(ResultSet rs, int rowNum)
							throws SQLException {

						MagUser user = new MagUser();
						user.setId(rs.getInt(1));
						user.setUsername(rs.getString(2));
						user.setPassword(rs.getString(3));
						user.setScreenname(rs.getString(5));
						user.setEnabled(rs.getBoolean(4));
						user.setIp(rs.getString(6));
						user.setJoinDate(rs.getDate(7).getTime());
						return user;
					}

				});
	}

	public MagUser loadUserById(int id)
			throws UsernameNotFoundException, DataAccessException {
		List<MagUser> users = loadUsersByUserId(id);

		if (users.size() == 0) {
			throw new UsernameNotFoundException("No user in repository " + id);
		}

		MagUser user = users.get(0); // contains no GrantedAuthority[]

		List<GrantedAuthority> dbAuths = loadUserAuthorities(user.getId());
		if (dbAuths.size() == 0) {
			throw new UsernameNotFoundException("no authority for username "
					+ id);
		}

		user.setAuthorities(dbAuths);
		return user;
	}

	protected List<MagUser> loadUsersByUserId(int id) {
		return getJdbcTemplate().query(usersByIdSql, new Integer[] { id },
				new RowMapper<MagUser>() {
					public MagUser mapRow(ResultSet rs, int rowNum)
							throws SQLException {

						MagUser user = new MagUser();
						user.setId(rs.getInt(1));
						user.setUsername(rs.getString(2));
						user.setPassword(rs.getString(3));
						user.setScreenname(rs.getString(5));
						user.setEnabled(rs.getBoolean(4));
						user.setIp(rs.getString(6));
						user.setJoinDate(rs.getDate(7).getTime());
						return user;
					}

				});
	}

	/**
	 * Loads authorities by executing the SQL from
	 * <tt>authoritiesByUsernameQuery</tt>.
	 * 
	 * @return a list of GrantedAuthority objects for the user
	 */
	protected List<GrantedAuthority> loadUserAuthorities(int id) {
		return getJdbcTemplate().query(authoritiesFromIdSql,
				new Integer[] { id }, new RowMapper<GrantedAuthority>() {
					public GrantedAuthority mapRow(ResultSet rs, int rowNum)
							throws SQLException {
						String roleName = rs.getString(2);
						SimpleGrantedAuthority authority = new SimpleGrantedAuthority(
								roleName);

						return authority;
					}
				});
	}

	@Override
	public void deleteUser(String username) {
		int id = getJdbcTemplate().queryForObject(
				"select id from users where username = ?",
				new String[] { username }, new RowMapper<Integer>() {
					public Integer mapRow(ResultSet rs, int rowNum)
							throws SQLException {
						return rs.getInt(1);
					}
				});
		deleteUserAuthorities(id);

		getJdbcTemplate().update(deleteUserSql, new Object[] { username });
		userCache.removeUserFromCache(username);
	}

	@Override
	public void changePassword(String oldPassword, String newPassword)
			throws AuthenticationException {
		Authentication currentUser = SecurityContextHolder.getContext()
				.getAuthentication();

		if (currentUser == null) {
			// This would indicate bad coding somewhere
			throw new AccessDeniedException(
					"Can't change password as no Authentication object found in context "
							+ "for current user.");
		}

		String username = currentUser.getName();

		// If an authentication manager has been set, re-authenticate the user
		// with the supplied password.
		if (authenticationManager != null) {
			logger.debug("Reauthenticating user '" + username
					+ "' for password change request.");

			authenticationManager
					.authenticate(new UsernamePasswordAuthenticationToken(
							username, oldPassword));
		} else {
			logger.debug("No authentication manager set. Password won't be re-checked.");
		}

		logger.debug("Changing password for user '" + username + "'");

		getJdbcTemplate().update(changePasswordSql, newPassword, username);

		SecurityContextHolder.getContext().setAuthentication(
				createNewAuthentication(currentUser, newPassword));

		userCache.removeUserFromCache(username);
	}

	protected Authentication createNewAuthentication(
			Authentication currentAuth, String newPassword) {
		UserDetails user = loadUserByUsername(currentAuth.getName());

		UsernamePasswordAuthenticationToken newAuthentication = new UsernamePasswordAuthenticationToken(
				user, user.getPassword(), user.getAuthorities());
		newAuthentication.setDetails(currentAuth.getDetails());

		return newAuthentication;
	}

	@Override
	public boolean userExists(String username) {
		List<String> users = getJdbcTemplate().queryForList(userExistsSql,
				new String[] { username }, String.class);

		if (users.size() > 1) {
			throw new IncorrectResultSizeDataAccessException(
					"More than one user found with name '" + username + "'", 1);
		}

		return users.size() == 1;
	}

	/**
	 * Returns screen name for a given user name
	 * 
	 * @param userName
	 * @return screename
	 */
	public final String getScreenName(String userName) {
		return getJdbcTemplate().queryForObject(usersByUsernameSql,
				new String[] { userName }, new RowMapper<String>() {
					public String mapRow(ResultSet rs, int rowNum)
							throws SQLException {
						return rs.getString(5);
					}
				});
	}

	/**
	 * @param screenName
	 * @returns userName corresponding to the screenName
	 */
	public final String getUserName(String screenName) {
		return getJdbcTemplate().queryForObject(selectUserNameSql,
				new String[] { screenName }, new RowMapper<String>() {
					public String mapRow(ResultSet rs, int rowNum)
							throws SQLException {
						return rs.getString(1);
					}
				});
	}

	public final boolean screenNameExists(String screenName) {
		String userName = getUserName(screenName);
		if (userName != null) {
			return true;
		}

		return false;
	}

	/**
	 * Utility method to list all user details - should be able to sort by join
	 * date, username etc
	 * 
	 * @return List of user details
	 */
	public final List<UserDetails> listAllUsers(int start, int length) {

		List<UserDetails> users = getJdbcTemplate().query(selectAllUsersSql,
				new Object[] { start, length }, new RowMapper<UserDetails>() {
					public UserDetails mapRow(ResultSet rs, int rowNum)
							throws SQLException {
						MagUser user = new MagUser();
						user.setId(rs.getInt(1));
						user.setUsername(rs.getString(2));
						user.setScreenname(rs.getString(3));
						user.setEnabled(rs.getBoolean(4));

						user.setIp(rs.getString(5));
						user.setJoinDate(rs.getDate(6).getTime());

						return user;
					}
				});
		return users;
	}

	/**
	 * List with condtion
	 * 
	 * @param start
	 * @param length
	 * @param searchField
	 * @param searchString
	 * @param searchOper
	 * @return
	 */
	public final List<UserDetails> listAllUsers(int start, int length,
			String searchField, String searchString, String searchOper) {
		String query = selectAllUsersSql + " AND username = ?";

		List<UserDetails> users = getJdbcTemplate().query(query,
				new Object[] { start, length, searchString },
				new RowMapper<UserDetails>() {
					public UserDetails mapRow(ResultSet rs, int rowNum)
							throws SQLException {
						MagUser user = new MagUser();
						user.setId(rs.getInt(1));
						user.setUsername(rs.getString(2));
						user.setScreenname(rs.getString(3));
						user.setEnabled(rs.getBoolean(4));

						user.setIp(rs.getString(5));
						user.setJoinDate(rs.getDate(6).getTime());

						return user;
					}
				});
		return users;
	}

	public final int getUserCount() {
		if (userCount == 0) {
			userCount = getJdbcTemplate().queryForObject(userCountSQL,

			new RowMapper<Integer>() {
				public Integer mapRow(ResultSet rs, int rowNum)
						throws SQLException {
					return rs.getInt(1);
				}
			});
		}
		return userCount;
	}

	//
	// /**
	// * Find the principal (first) group for the user. TODO Need to check
	// groups
	// * are always inserted in order, otherwise add an autoinc field.
	// *
	// * @param userName
	// * @return Principal Group
	// */
	// public String getPrincipalGroup(String userName) {
	// UserDetails ud = loadUserByUsername(userName);
	//
	// Collection<? extends GrantedAuthority> authorities = ud
	// .getAuthorities();
	// String group = null;
	// for (GrantedAuthority authority : authorities) {
	// group = authority.getAuthority();
	// if (!group.startsWith("ROLE_")) {
	// break;
	// }
	// }
	//
	// return group;
	// }
	//

	public ArrayList<String> listUserGroup(int id) {

		ArrayList<String> groups = new ArrayList<String>();

		Collection<? extends GrantedAuthority> authorities = loadGrantedAuthorities(id);
		for (GrantedAuthority authority : authorities) {
			groups.add(authority.getAuthority());
		}

		return groups;
	}

	private List<GrantedAuthority> loadGrantedAuthorities(int id) {
		List<GrantedAuthority> dbAuths = loadUserAuthorities(id);
		if (dbAuths.size() == 0) {
			throw new UsernameNotFoundException("no authority for id " + id);
		}

		return dbAuths;
	}

	private void validateUserDetails(UserDetails user) {
		Assert.hasText(user.getUsername(), "Username may not be empty or null");
		validateAuthorities(user.getAuthorities());
	}

	private void validateAuthorities(
			Collection<? extends GrantedAuthority> authorities) {
		Assert.notNull(authorities, "Authorities list must not be null");

		for (GrantedAuthority authority : authorities) {
			Assert.notNull(authority, "Authorities list contains a null entry");
			Assert.hasText(authority.getAuthority(),
					"getAuthority() method must return a non-empty string");
		}
	}

	@Override
	public List<String> findAllGroups() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public List<String> findUsersInGroup(String groupName) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public void createGroup(String groupName, List<GrantedAuthority> authorities) {
		// TODO Auto-generated method stub

	}

	@Override
	public void deleteGroup(String groupName) {
		// TODO Auto-generated method stub

	}

	@Override
	public void renameGroup(String oldName, String newName) {
		// TODO Auto-generated method stub

	}

	@Override
	public void addUserToGroup(String username, String group) {
		// TODO Auto-generated method stub

	}

	@Override
	public void removeUserFromGroup(String username, String groupName) {
		// TODO Auto-generated method stub

	}

	@Override
	public List<GrantedAuthority> findGroupAuthorities(String groupName) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public void addGroupAuthority(String groupName, GrantedAuthority authority) {
		// TODO Auto-generated method stub

	}

	@Override
	public void removeGroupAuthority(String groupName,
			GrantedAuthority authority) {
		// TODO Auto-generated method stub

	}

}